<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      svg {
        margin-right: 10px;
      }
    </style>
  </head>
  <body>
    <body>
      <svg id="firstSvg" width="800" height="600" style="border: 1px solid black">
        <circle cx="20" cy="20" r="2" />
        <circle cx="120" cy="120" r="2" />
      </svg>
    </body>
    <script>
      function generateorXY(arrs) {
        return {
          x: arrs[0],
          y: arrs[1],
        }
      }

      /**
       * 简化用户路径，移除冗余的点
       */
      function simplifyUserPath(path, threshold = 10) {
        if (path.length <= 2) return path

        const simplified = [path[0]]

        for (let i = 1; i < path.length - 1; i++) {
          const prev = generateorXY(simplified[simplified.length - 1])
          const current = generateorXY(path[i])
          const next = generateorXY(path[i + 1])

          // 计算当前点到前一点的距离
          const distToPrev = Math.sqrt(
            Math.pow(current.x - prev.x, 2) + Math.pow(current.y - prev.y, 2)
          )

          // 如果距离太小，跳过这个点
          if (distToPrev < threshold) {
            continue
          }

          // 检查是否形成明显的转折
          const angle1 = Math.atan2(current.y - prev.y, current.x - prev.x)
          const angle2 = Math.atan2(next.y - current.y, next.x - current.x)
          const angleDiff = Math.abs(angle1 - angle2)

          // 如果角度变化明显，保留这个点
          if (angleDiff > Math.PI / 6) {
            // 30度以上的转折
            simplified.push([current.x, current.y])
          }
        }

        simplified.push(path[path.length - 1])
        return simplified
      }
      /**
       * 确保路径以指定起点开始（预览模式专用）
       */
      function constrainPathToStartpoint(path, startPoint) {
        if (path.length === 0) return [startPoint]

        const constrainedPath = [startPoint]

        // 添加用户绘制的所有点
        for (const point of path) {
          // 跳过与起点过于接近的点
          const lastPoint = generateorXY(constrainedPath[constrainedPath.length - 1])
          const transPoint = generateorXY(point)
          const distance = Math.sqrt(
            Math.pow(transPoint.x - lastPoint.x, 2) + Math.pow(transPoint.y - lastPoint.y, 2)
          )

          if (distance > 10) {
            // 至少10像素的距离
            constrainedPath.push(point)
          }
        }

        return constrainedPath
      }
      /**
       * 将路径转换为横折格式（只包含水平和垂直线段）
       */
      function convertToOrthogonalSegments(path) {
        if (path.length <= 1) return path

        const orthogonalPath = [path[0]]

        for (let i = 1; i < path.length; i++) {
          const prev = generateorXY(orthogonalPath[orthogonalPath.length - 1])
          const current = generateorXY(path[i])

          const deltaX = Math.abs(current.x - prev.x)
          const deltaY = Math.abs(current.y - prev.y)

          // 决定先走水平还是垂直
          if (deltaX > deltaY) {
            // 先水平移动，后垂直移动
            if (Math.abs(current.x - prev.x) > 5) {
              // 添加水平中间点
              orthogonalPath.push([current.x, prev.y])
            }
            if (Math.abs(current.y - prev.y) > 5) {
              // 添加目标点
              orthogonalPath.push([current.x, current.y])
            }
          } else {
            // 先垂直移动，后水平移动
            if (Math.abs(current.y - prev.y) > 5) {
              // 添加垂直中间点
              orthogonalPath.push([prev.x, current.y])
            }
            if (Math.abs(current.x - prev.x) > 5) {
              // 添加目标点
              orthogonalPath.push([current.x, current.y])
            }
          }
        }

        return orthogonalPath
      }
      /**
       * 优化横折路径，移除不必要的转折点
       */
      function optimizeOrthogonalPath(path) {
        if (path.length <= 2) return path

        const optimized = [path[0]]

        for (let i = 1; i < path.length - 1; i++) {
          const prev = generateorXY(optimized[optimized.length - 1])
          const current = generateorXY(path[i])
          const next = generateorXY(path[i + 1])

          // 检查三点是否共线
          const isHorizontalLine =
            Math.abs(prev.y - current.y) < 1 && Math.abs(current.y - next.y) < 1
          const isVerticalLine =
            Math.abs(prev.x - current.x) < 1 && Math.abs(current.x - next.x) < 1

          // 如果不共线，保留转折点
          if (!isHorizontalLine && !isVerticalLine) {
            optimized.push([current.x, current.y])
          }
        }

        optimized.push(path[path.length - 1])
        return optimized
      }

      function convertUserPathToOrthogonalPreview(userPath, sourceX, sourceY) {
        if (!userPath || userPath.length < 1) {
          return [sourceX, sourceY]
        }

        // 如果只有一个点，直接返回起点到该点的路径
        if (userPath.length === 1) {
          return [[sourceX, sourceY], userPath[0]]
        }

        // 第一步：简化用户路径，移除冗余点
        const simplifiedPath = simplifyUserPath(userPath, 15)

        // 第二步：确保路径以起点开始，但不强制连接到特定终点
        const constrainedPath = constrainPathToStartpoint(simplifiedPath, [sourceX, sourceY])

        // 第三步：将路径转换为横折格式（只保留水平和垂直线段）
        const orthogonalPath = convertToOrthogonalSegments(constrainedPath)

        // 第四步：优化路径，移除不必要的转折点
        const optimizedPath = optimizeOrthogonalPath(orthogonalPath)

        return optimizedPath
      }

      /**
       * 计算边的折线路径点
       * 根据不同的布局方向和节点相对位置，计算出合适的折线路径点
       * @param model - 边模型，包含边的基本信息如源节点ID、目标节点ID等
       * @param nodes - 节点数据数组，包含所有节点的布局信息
       * @returns 计算后的路径点数组，如果无法计算则返回undefined
       */
      function calcPointsList(model, nodes) {
        // 初始化路径点列表，用于存储计算出的路径点
        const pointsList = []

        // 获取源节点和目标节点的模型与布局数据
        // 通过LogicFlow实例获取源节点模型，用于获取节点尺寸等信息
        const sourceNodeModel = this.lf.getNodeModelById(model.sourceNodeId)
        // 通过LogicFlow实例获取目标节点模型，用于获取节点尺寸等信息
        const targetNodeModel = this.lf.getNodeModelById(model.targetNodeId)
        // 在布局后的节点数据中查找源节点，用于获取节点位置信息
        const newSourceNodeData = nodes.find((node) => node.id === model.sourceNodeId)
        // 在布局后的节点数据中查找目标节点，用于获取节点位置信息
        const newTargetNodeData = nodes.find((node) => node.id === model.targetNodeId)

        // 数据验证，确保源节点和目标节点的模型和布局数据都存在
        if (!sourceNodeModel || !targetNodeModel || !newSourceNodeData || !newTargetNodeData) {
          // 如果任一节点数据缺失，返回undefined让LogicFlow自行处理路径计算
          return undefined
        }

        // 获取折线偏移量，默认为50，用于控制折线转折点距离节点的距离
        // 这个值决定了连线的弯曲程度和美观度
        const offset = Number(model.offset) || 50

        // 处理从左到右(LR)布局的边路径计算
        // 只有在布局方向为LR且边类型为折线时才进行处理
        if (this.option.rankdir === 'LR' && model.modelType === 'polyline-edge') {
          // 正向连线：源节点在目标节点左侧
          // 当源节点x坐标小于目标节点x坐标时，认为是正向连接
          if (newSourceNodeData.x < newTargetNodeData.x) {
            // 从源节点右侧中心出发，计算起点坐标
            pointsList.push({
              x: newSourceNodeData.x + sourceNodeModel.width / 2,
              y: newSourceNodeData.y,
            })
            // 向右延伸一段距离，创建第一个转折点
            pointsList.push({
              x: newSourceNodeData.x + sourceNodeModel.width / 2 + offset,
              y: newSourceNodeData.y,
            })
            // 垂直移动到目标节点的高度，创建第二个转折点
            pointsList.push({
              x: newSourceNodeData.x + sourceNodeModel.width / 2 + offset,
              y: newTargetNodeData.y,
            })
            // 连接到目标节点左侧中心，计算终点坐标
            pointsList.push({
              x: newTargetNodeData.x - targetNodeModel.width / 2,
              y: newTargetNodeData.y,
            })

            // 对路径点进行优化处理，移除冗余的点后返回结果
            return this.pointFilter(pointsList)
          }

          // 反向连线：源节点在目标节点右侧
          // 当源节点x坐标大于目标节点x坐标时，认为是反向连接
          if (newSourceNodeData.x > newTargetNodeData.x) {
            // 根据节点相对Y轴位置选择不同路径
            // 当源节点y坐标大于等于目标节点y坐标时，源节点在目标节点的右下方
            if (newSourceNodeData.y >= newTargetNodeData.y) {
              // 源节点在目标节点的右下方，从源节点上方出发
              pointsList.push({
                x: newSourceNodeData.x,
                y: newSourceNodeData.y + sourceNodeModel.height / 2,
              })
              // 向下延伸一段距离，创建第一个转折点
              pointsList.push({
                x: newSourceNodeData.x,
                y: newSourceNodeData.y + sourceNodeModel.height / 2 + offset,
              })
              // 水平移动到目标节点位置，创建第二个转折点
              pointsList.push({
                x: newTargetNodeData.x,
                y: newSourceNodeData.y + sourceNodeModel.height / 2 + offset,
              })
              // 连接到目标节点上方中心，计算终点坐标
              pointsList.push({
                x: newTargetNodeData.x,
                y: newTargetNodeData.y + targetNodeModel.height / 2,
              })
            } else {
              // 源节点在目标节点的右上方，从源节点下方出发
              pointsList.push({
                x: newSourceNodeData.x,
                y: newSourceNodeData.y - sourceNodeModel.height / 2,
              })
              // 向上延伸一段距离，创建第一个转折点
              pointsList.push({
                x: newSourceNodeData.x,
                y: newSourceNodeData.y - sourceNodeModel.height / 2 - offset,
              })
              // 水平移动到目标节点位置，创建第二个转折点
              pointsList.push({
                x: newTargetNodeData.x,
                y: newSourceNodeData.y - sourceNodeModel.height / 2 - offset,
              })
              // 连接到目标节点下方中心，计算终点坐标
              pointsList.push({
                x: newTargetNodeData.x,
                y: newTargetNodeData.y - targetNodeModel.height / 2,
              })
            }

            // 对路径点进行优化处理，移除冗余的点后返回结果
            return this.pointFilter(pointsList)
          }
        }

        // 处理从上到下(TB)布局的边路径计算
        // 只有在布局方向为TB且边类型为折线时才进行处理
        if (this.option.rankdir === 'TB' && model.modelType === 'polyline-edge') {
          // 正向连线：源节点在目标节点上方
          // 当源节点y坐标小于目标节点y坐标时，认为是正向连接
          if (newSourceNodeData.y < newTargetNodeData.y) {
            // 从源节点底部中心出发，计算起点坐标
            pointsList.push({
              x: newSourceNodeData.x,
              y: newSourceNodeData.y + sourceNodeModel.height / 2,
            })
            // 向下延伸一段距离，创建第一个转折点
            pointsList.push({
              x: newSourceNodeData.x,
              y: newSourceNodeData.y + sourceNodeModel.height / 2 + offset,
            })
            // 水平移动到目标节点的位置，创建第二个转折点
            pointsList.push({
              x: newTargetNodeData.x,
              y: newSourceNodeData.y + sourceNodeModel.height / 2 + offset,
            })
            // 连接到目标节点顶部中心，计算终点坐标
            pointsList.push({
              x: newTargetNodeData.x,
              y: newTargetNodeData.y - targetNodeModel.height / 2,
            })

            // 对路径点进行优化处理，移除冗余的点后返回结果
            return this.pointFilter(pointsList)
          }

          // 反向连线：源节点在目标节点下方
          // 当源节点y坐标大于目标节点y坐标时，认为是反向连接
          if (newSourceNodeData.y > newTargetNodeData.y) {
            // 当源节点x坐标大于等于目标节点x坐标时，源节点在目标节点右下方
            if (newSourceNodeData.x >= newTargetNodeData.x) {
              // 源节点在目标节点右下方，从源节点右侧出发
              pointsList.push({
                x: newSourceNodeData.x + sourceNodeModel.width / 2,
                y: newSourceNodeData.y,
              })
              // 向右延伸一段距离，创建第一个转折点
              pointsList.push({
                x: newSourceNodeData.x + sourceNodeModel.width / 2 + offset,
                y: newSourceNodeData.y,
              })
              // 垂直移动到目标节点位置，创建第二个转折点
              pointsList.push({
                x: newSourceNodeData.x + sourceNodeModel.width / 2 + offset,
                y: newTargetNodeData.y,
              })
              // 连接到目标节点右侧中心，计算终点坐标
              pointsList.push({
                x: newTargetNodeData.x + targetNodeModel.width / 2,
                y: newTargetNodeData.y,
              })
            } else {
              // 源节点在目标节点左下方，从源节点左侧出发
              pointsList.push({
                x: newSourceNodeData.x - sourceNodeModel.width / 2,
                y: newSourceNodeData.y,
              })
              // 向左延伸一段距离，创建第一个转折点
              pointsList.push({
                x: newSourceNodeData.x - sourceNodeModel.width / 2 - offset,
                y: newSourceNodeData.y,
              })
              // 垂直移动到目标节点位置，创建第二个转折点
              pointsList.push({
                x: newSourceNodeData.x - sourceNodeModel.width / 2 - offset,
                y: newTargetNodeData.y,
              })
              // 连接到目标节点左侧中心，计算终点坐标
              pointsList.push({
                x: newTargetNodeData.x - targetNodeModel.width / 2,
                y: newTargetNodeData.y,
              })
            }

            // 对路径点进行优化处理，移除冗余的点后返回结果
            return this.pointFilter(pointsList)
          }
        }

        // 无法确定路径时返回undefined，让LogicFlow自行处理
        // 当不满足上述任何条件时，返回undefined，交由LogicFlow框架处理路径计算
        return undefined
      }
      function stabilizeDrawingPath(
        rawPath,
        snapThreshold = 20, // 吸附阈值
        angleThreshold = 15 // 角度阈值（度）
      ) {
        if (rawPath.length < 2) return rawPath

        const stabilizedPath = [rawPath[0]]

        for (let i = 1; i < rawPath.length; i++) {
          const current = generateorXY(rawPath[i])
          const previous = generateorXY(stabilizedPath[stabilizedPath.length - 1])

          // 计算移动向量
          const deltaX = current.x - previous.x
          const deltaY = current.y - previous.y
          const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)

          // 如果移动距离太小，跳过这个点
          if (distance < 5) {
            continue
          }

          // 计算移动角度
          const angle = Math.atan2(deltaY, deltaX)
          const angleDegrees = (angle * 180) / Math.PI

          // 吸附到水平或垂直方向
          const stabilizedPoint = { ...current }

          // 检查是否接近水平（0°, ±180°）
          if (
            Math.abs(angleDegrees) < angleThreshold ||
            Math.abs(Math.abs(angleDegrees) - 180) < angleThreshold
          ) {
            // 吸附到水平线
            if (Math.abs(deltaY) < snapThreshold) {
              stabilizedPoint.y = previous.y
            }
          }
          // 检查是否接近垂直（±90°）
          else if (Math.abs(Math.abs(angleDegrees) - 90) < angleThreshold) {
            // 吸附到垂直线
            if (Math.abs(deltaX) < snapThreshold) {
              stabilizedPoint.x = previous.x
            }
          }
          // console.log(stabilizedPoint)

          stabilizedPoint.x && stabilizedPath.push([stabilizedPoint.x, stabilizedPoint.y])
        }

        return stabilizedPath
      }
    </script>
    <script type="module">
      import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm'

      const width = 800
      const height = 600

      // 鼠标移动曲线
      function drawLine() {
        const svg = d3.select('#firstSvg')

        let isDrawing = false
        let lineData = []
        let drawLineData = []
        let sourcePoint = []
        let linePath

        // Create a line generator
        const lineGenerator = d3
          .line()
          .x((d) => d[0])
          .y((d) => d[1])
          .curve(d3.curveLinear) // Or d3.curveBasis for a smoother line

        svg.on('mousedown', (event) => {
          isDrawing = true
          sourcePoint = d3.pointer(event)
          lineData = [sourcePoint] // Start the line with the current mouse position
          // Append a new path element to the SVG
          linePath = svg
            .append('path')
            .attr('stroke', 'blue')
            .attr('stroke-width', 2)
            .attr('fill', 'none')
        })

        svg.on('mousemove', (event) => {
          if (!isDrawing) return

          const position = d3.pointer(event)
          const lastRawPoint = lineData[lineData.length - 1]

          // 只有当移动距离足够大时才添加新点
          const distance = Math.sqrt(
            Math.pow(position[0] - lastRawPoint[0], 2) + Math.pow(position[1] - lastRawPoint[1], 2)
          )
          if (distance > 3) {
            // Add the current point to the line data
            lineData.push(position)
            const stabilizeData = stabilizeDrawingPath(lineData)
            // console.log(JSON.stringify(lineData), JSON.stringify(stabilizeData))

            drawLineData = convertUserPathToOrthogonalPreview(
              stabilizeData,
              sourcePoint[0],
              sourcePoint[1]
            )
            // console.log(drawLineData)

            linePath.datum(drawLineData)

            // Update the path's 'd' attribute using the line generator
          }
          drawLineData.length > 0 && linePath.attr('d', lineGenerator)
        })

        svg.on('mouseup', () => {
          isDrawing = false
          lineData.length = 0
          drawLineData.legnth = 0
        })
      }
      // d3 draw custom straight line with mousemove
      function drawSLine() {
        const svg = d3
          .select('body')
          .append('svg')
          .attr('width', width)
          .attr('height', height)
          .style('border', '1px solid black')

        let startPoint = null
        let lineElement = null

        svg.on('mousedown', (event) => {
          // Store the starting coordinates
          startPoint = d3.pointer(event)

          // If a previous line exists, remove it before drawing a new one
          if (lineElement) {
            lineElement.remove()
          }

          // Append a new line element to the SVG
          lineElement = svg
            .append('line')
            .attr('x1', startPoint[0])
            .attr('y1', startPoint[1])
            .attr('x2', startPoint[0]) // x2 starts at x1
            .attr('y2', startPoint[1]) // y2 starts at y1
            .attr('stroke', 'blue')
            .attr('stroke-width', 2)
        })

        svg.on('mousemove', (event) => {
          // Only update the line if a start point has been defined
          if (!startPoint) return

          // Get the current mouse coordinates
          const currentPoint = d3.pointer(event)

          // Update the line's ending coordinates
          lineElement.attr('x2', currentPoint[0]).attr('y2', currentPoint[1])
        })

        svg.on('mouseup', () => {
          // Reset the starting point to finalize the line
          startPoint = null
        })
      }

      // d3 draw custom continuous straight line with mousemove
      function drawConSLine() {
        const svg = d3
          .select('body')
          .append('svg')
          .attr('width', width)
          .attr('height', height)
          .style('border', '1px solid black')

        let isDrawing = false
        let lineData = []
        let linePath

        // Create a line generator for a straight line
        const lineGenerator = d3
          .line()
          .x((d) => d[0])
          .y((d) => d[1])
          .curve(d3.curveLinear)

        svg.on('mousedown', (event) => {
          isDrawing = true
          // Get the start point of the new line segment
          const startPoint = d3.pointer(event)

          // If no line exists, create one and start the data array.
          if (lineData.length === 0) {
            lineData.push(startPoint)
            linePath = svg
              .append('path')
              .datum(lineData)
              .attr('stroke', 'blue')
              .attr('stroke-width', 2)
              .attr('fill', 'none')
          } else {
            // For subsequent clicks, add the new point and redraw.
            lineData.push(startPoint)
          }

          // Update the path with the new line segment
          linePath.attr('d', lineGenerator)
        })

        svg.on('mousemove', (event) => {
          if (!isDrawing) return

          // Update the last point in the data array with the current mouse position
          lineData[lineData.length - 1] = d3.pointer(event)

          // Redraw the entire path
          linePath.attr('d', lineGenerator)
        })

        svg.on('mouseup', () => {
          isDrawing = false
        })
      }

      // d3 draw custom continuous right angle lines with mousemove
      function drawLine4() {
        const svg = d3
          .select('body')
          .append('svg')
          .attr('width', width)
          .attr('height', height)
          .style('border', '1px solid black')

        let isDrawing = false
        let lineData = []
        let linePath

        // Create a line generator for a straight line
        const lineGenerator = d3
          .line()
          .x((d) => d[0])
          .y((d) => d[1])
          .curve(d3.curveLinear)

        svg.on('mousedown', (event) => {
          isDrawing = true
          const startPoint = d3.pointer(event)

          // If this is the start of a new line, initialize data and create a path
          if (lineData.length === 0) {
            lineData.push(startPoint)
            linePath = svg
              .append('path')
              .datum(lineData)
              .attr('stroke', 'blue')
              .attr('stroke-width', 2)
              .attr('fill', 'none')
          } else {
            // For subsequent clicks, add a new segment
            lineData.push(startPoint)
            // After adding a new point, the previous point becomes the corner.
            // The next mousemove will modify the latest point.
            linePath.attr('d', lineGenerator)
          }
        })

        svg.on('mousemove', (event) => {
          if (!isDrawing || lineData.length === 0) return

          const currentPoint = d3.pointer(event)
          const lastPoint = lineData[lineData.length - 1]
          const previousPoint = lineData[lineData.length - 2] || lastPoint

          // Determine if the horizontal or vertical distance is greater
          const dx = Math.abs(currentPoint[0] - previousPoint[0])
          const dy = Math.abs(currentPoint[1] - previousPoint[1])

          // Update the line segment to be either horizontal or vertical
          if (dx > dy) {
            // horizontal
            console.log('horizontal')

            lastPoint[0] = currentPoint[0]
            lastPoint[1] = previousPoint[1]
          } else {
            // vertical
            console.log('vertical')
            lastPoint[0] = previousPoint[0]
            lastPoint[1] = currentPoint[1]
          }

          // Redraw the entire path
          linePath.attr('d', lineGenerator)
        })

        svg.on('mouseup', () => {
          isDrawing = false
        })
      }

      // custom curve
      function drawLine5() {
        // Initialize SVG and line generator
        const svg = d3
          .select('body')
          .append('svg')
          .attr('width', width)
          .attr('height', height)
          .attr('fill', 'none')
          .attr('stroke', 'blue')
          .style('border', '1px solid black')
        const line = d3
          .line()
          .x((d) => d[0])
          .y((d) => d[1])
          .curve(d3.curveLinear) // Placeholder, will be replaced with custom logic

        let isDrawing = false
        let points = [] // Store points for the right-angle line
        svg.on('mousedown', function () {
          isDrawing = true
        })
        svg.on('mouseup', function () {
          isDrawing = false
        })
        svg.on('mousemove', function (event) {
          if (isDrawing) {
            const [x, y] = d3.pointer(event, this) // Get mouse coordinates

            // Add new point to store for the right-angle line
            points.push([x, y])

            // Filter and process points to create right angles (simplified)
            // This is where you'd implement the core logic for right-angle segments
            const rightAnglePoints = points.reduce((acc, current, i) => {
              if (i === 0) {
                return acc.concat([current]) // Always start with the first point
              }

              // Add a point that creates a right angle based on the last two points
              acc.push([acc[i - 1][0], current[1]])
              acc.push(current)
              return acc
            }, [])

            // Bind the right-angle points to the line
            svg.selectAll('path').data([rightAnglePoints]).join('path').attr('d', line)
          }
        })
      }

      drawLine()
      drawSLine()
      drawConSLine()
      drawLine4()
      drawLine5()
    </script>
  </body>
</html>
